/******************************************************************************* * Copyright (c) 2015 Jeff Martin. * All rights reserved. This program and the accompanying materials * are made available under the terms of the GNU Lesser General Public * License v3.0 which accompanies this distribution, and is available at * http://www.gnu.org/licenses/lgpl.html * * Contributors: * Jeff Martin - initial API and implementation ******************************************************************************/ package cuchaz.enigma.convert; import java.util.*; import java.util.jar.JarFile; import com.google.common.collect.BiMap; import com.google.common.collect.HashMultimap; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.google.common.collect.Multimap; import com.google.common.collect.Sets; import cuchaz.enigma.Deobfuscator; import cuchaz.enigma.analysis.JarIndex; import cuchaz.enigma.convert.ClassNamer.SidedClassNamer; import cuchaz.enigma.mapping.*; public class MappingsConverter { public static ClassMatches computeClassMatches(JarFile sourceJar, JarFile destJar, Mappings mappings) { // index jars System.out.println("Indexing source jar..."); JarIndex sourceIndex = new JarIndex(); sourceIndex.indexJar(sourceJar, false); System.out.println("Indexing dest jar..."); JarIndex destIndex = new JarIndex(); destIndex.indexJar(destJar, false); // compute the matching ClassMatching matching = computeMatching(sourceJar, sourceIndex, destJar, destIndex, null); return new ClassMatches(matching.matches()); } public static ClassMatching computeMatching(JarFile sourceJar, JarIndex sourceIndex, JarFile destJar, JarIndex destIndex, BiMap<ClassEntry, ClassEntry> knownMatches) { System.out.println("Iteratively matching classes"); ClassMatching lastMatching = null; int round = 0; SidedClassNamer sourceNamer = null; SidedClassNamer destNamer = null; for(boolean useReferences : Arrays.asList(false, true)) { int numUniqueMatchesLastTime = 0; if(lastMatching != null) numUniqueMatchesLastTime = lastMatching.uniqueMatches().size(); while(true) { System.out.println("Round " + (++round) + "..."); // init the matching with identity settings ClassMatching matching = new ClassMatching(new ClassIdentifier(sourceJar, sourceIndex, sourceNamer, useReferences), new ClassIdentifier(destJar, destIndex, destNamer, useReferences)); if(knownMatches != null) matching.addKnownMatches(knownMatches); if(lastMatching == null) // search all classes matching.match(sourceIndex.getObfClassEntries(), destIndex.getObfClassEntries()); else { // we already know about these matches from last time matching.addKnownMatches(lastMatching.uniqueMatches()); // search unmatched and ambiguously-matched classes matching.match(lastMatching.unmatchedSourceClasses(), lastMatching.unmatchedDestClasses()); for(ClassMatch match : lastMatching.ambiguousMatches()) matching.match(match.sourceClasses, match.destClasses); } System.out.println(matching); BiMap<ClassEntry, ClassEntry> uniqueMatches = matching.uniqueMatches(); // did we match anything new this time? if(uniqueMatches.size() > numUniqueMatchesLastTime) { numUniqueMatchesLastTime = uniqueMatches.size(); lastMatching = matching; }else break; // update the namers ClassNamer namer = new ClassNamer(uniqueMatches); sourceNamer = namer.getSourceNamer(); destNamer = namer.getDestNamer(); } } return lastMatching; } public static Mappings newMappings(ClassMatches matches, Mappings oldMappings, Deobfuscator sourceDeobfuscator, Deobfuscator destDeobfuscator) { // sort the unique matches by size of inner class chain Multimap<Integer, java.util.Map.Entry<ClassEntry, ClassEntry>> matchesByDestChainSize = HashMultimap.create(); for(java.util.Map.Entry<ClassEntry, ClassEntry> match : matches .getUniqueMatches().entrySet()) { int chainSize = destDeobfuscator.getJarIndex() .getObfClassChain(match.getValue()).size(); matchesByDestChainSize.put(chainSize, match); } // build the mappings (in order of small-to-large inner chains) Mappings newMappings = new Mappings(); List<Integer> chainSizes = Lists.newArrayList(matchesByDestChainSize.keySet()); Collections.sort(chainSizes); for(int chainSize : chainSizes) for(java.util.Map.Entry<ClassEntry, ClassEntry> match : matchesByDestChainSize .get(chainSize)) { // get class info ClassEntry obfSourceClassEntry = match.getKey(); ClassEntry obfDestClassEntry = match.getValue(); List<ClassEntry> destClassChain = destDeobfuscator.getJarIndex().getObfClassChain( obfDestClassEntry); ClassMapping sourceMapping = sourceDeobfuscator.getMappings().getClassByObf( obfSourceClassEntry); if(sourceMapping == null) // if this class was never deobfuscated, don't try to match // it continue; // find out where to make the dest class mapping if(destClassChain.size() == 1) // not an inner class, add directly to mappings newMappings.addClassMapping(migrateClassMapping( obfDestClassEntry, sourceMapping, matches, false)); else { // inner class, find the outer class mapping ClassMapping destMapping = null; for(int i = 0; i < destClassChain.size() - 1; i++) { ClassEntry destChainClassEntry = destClassChain.get(i); if(destMapping == null) { destMapping = newMappings.getClassByObf(destChainClassEntry); if(destMapping == null) { destMapping = new ClassMapping( destChainClassEntry.getName()); newMappings.addClassMapping(destMapping); } }else { destMapping = destMapping .getInnerClassByObfSimple(destChainClassEntry .getInnermostClassName()); if(destMapping == null) { destMapping = new ClassMapping( destChainClassEntry.getName()); destMapping.addInnerClassMapping(destMapping); } } } destMapping.addInnerClassMapping(migrateClassMapping( obfDestClassEntry, sourceMapping, matches, true)); } } return newMappings; } private static ClassMapping migrateClassMapping(ClassEntry newObfClass, ClassMapping oldClassMapping, final ClassMatches matches, boolean useSimpleName) { ClassNameReplacer replacer = new ClassNameReplacer() { @Override public String replace(String className) { ClassEntry newClassEntry = matches.getUniqueMatches().get(new ClassEntry(className)); if(newClassEntry != null) return newClassEntry.getName(); return null; } }; ClassMapping newClassMapping; String deobfName = oldClassMapping.getDeobfName(); if(deobfName != null) { if(useSimpleName) deobfName = new ClassEntry(deobfName).getSimpleName(); newClassMapping = new ClassMapping(newObfClass.getName(), deobfName); }else newClassMapping = new ClassMapping(newObfClass.getName()); // copy fields for(FieldMapping fieldMapping : oldClassMapping.fields()) newClassMapping.addFieldMapping(new FieldMapping(fieldMapping, replacer)); // copy methods for(MethodMapping methodMapping : oldClassMapping.methods()) newClassMapping.addMethodMapping(new MethodMapping(methodMapping, replacer)); return newClassMapping; } public static void convertMappings(Mappings mappings, BiMap<ClassEntry, ClassEntry> changes) { // sort the changes so classes are renamed in the correct order // ie. if we have the mappings a->b, b->c, we have to apply b->c before // a->b LinkedHashMap<ClassEntry, ClassEntry> sortedChanges = Maps.newLinkedHashMap(); int numChangesLeft = changes.size(); while(!changes.isEmpty()) { Iterator<Map.Entry<ClassEntry, ClassEntry>> iter = changes.entrySet().iterator(); while(iter.hasNext()) { Map.Entry<ClassEntry, ClassEntry> change = iter.next(); if(changes.containsKey(change.getValue())) { sortedChanges.put(change.getKey(), change.getValue()); iter.remove(); } } // did we remove any changes? if(numChangesLeft - changes.size() > 0) // keep going numChangesLeft = changes.size(); else // can't sort anymore. There must be a loop break; } if(!changes.isEmpty()) throw new Error( "Unable to sort class changes! There must be a cycle."); // convert the mappings in the correct class order for(Map.Entry<ClassEntry, ClassEntry> entry : sortedChanges.entrySet()) mappings.renameObfClass(entry.getKey().getName(), entry.getValue() .getName()); } public static interface Doer<T extends Entry> { Collection<T> getDroppedEntries(MappingsChecker checker); Collection<T> getObfEntries(JarIndex jarIndex); Collection<? extends MemberMapping<T>> getMappings( ClassMapping destClassMapping); Set<T> filterEntries(Collection<T> obfEntries, T obfSourceEntry, ClassMatches classMatches); void setUpdateObfMember(ClassMapping classMapping, MemberMapping<T> memberMapping, T newEntry); boolean hasObfMember(ClassMapping classMapping, T obfEntry); void removeMemberByObf(ClassMapping classMapping, T obfEntry); } public static Doer<FieldEntry> getFieldDoer() { return new Doer<FieldEntry>() { @Override public Collection<FieldEntry> getDroppedEntries( MappingsChecker checker) { return checker.getDroppedFieldMappings().keySet(); } @Override public Collection<FieldEntry> getObfEntries(JarIndex jarIndex) { return jarIndex.getObfFieldEntries(); } @Override public Collection<? extends MemberMapping<FieldEntry>> getMappings( ClassMapping destClassMapping) { return (Collection<? extends MemberMapping<FieldEntry>>)destClassMapping .fields(); } @Override public Set<FieldEntry> filterEntries( Collection<FieldEntry> obfDestFields, FieldEntry obfSourceField, ClassMatches classMatches) { Set<FieldEntry> out = Sets.newHashSet(); for(FieldEntry obfDestField : obfDestFields) { Type translatedDestType = translate(obfDestField.getType(), classMatches .getUniqueMatches().inverse()); if(translatedDestType.equals(obfSourceField.getType())) out.add(obfDestField); } return out; } @Override public void setUpdateObfMember(ClassMapping classMapping, MemberMapping<FieldEntry> memberMapping, FieldEntry newField) { FieldMapping fieldMapping = (FieldMapping)memberMapping; classMapping.setFieldObfNameAndType(fieldMapping.getObfName(), fieldMapping.getObfType(), newField.getName(), newField.getType()); } @Override public boolean hasObfMember(ClassMapping classMapping, FieldEntry obfField) { return classMapping.getFieldByObf(obfField.getName(), obfField.getType()) != null; } @Override public void removeMemberByObf(ClassMapping classMapping, FieldEntry obfField) { classMapping.removeFieldMapping(classMapping.getFieldByObf( obfField.getName(), obfField.getType())); } }; } public static Doer<BehaviorEntry> getMethodDoer() { return new Doer<BehaviorEntry>() { @Override public Collection<BehaviorEntry> getDroppedEntries( MappingsChecker checker) { return checker.getDroppedMethodMappings().keySet(); } @Override public Collection<BehaviorEntry> getObfEntries(JarIndex jarIndex) { return jarIndex.getObfBehaviorEntries(); } @Override public Collection<? extends MemberMapping<BehaviorEntry>> getMappings( ClassMapping destClassMapping) { return (Collection<? extends MemberMapping<BehaviorEntry>>)destClassMapping .methods(); } @Override public Set<BehaviorEntry> filterEntries( Collection<BehaviorEntry> obfDestFields, BehaviorEntry obfSourceField, ClassMatches classMatches) { Set<BehaviorEntry> out = Sets.newHashSet(); for(BehaviorEntry obfDestField : obfDestFields) { Signature translatedDestSignature = translate(obfDestField.getSignature(), classMatches .getUniqueMatches().inverse()); if(translatedDestSignature == null && obfSourceField.getSignature() == null) out.add(obfDestField); else if(translatedDestSignature == null || obfSourceField.getSignature() == null) { // skip it }else if(translatedDestSignature.equals(obfSourceField .getSignature())) out.add(obfDestField); } return out; } @Override public void setUpdateObfMember(ClassMapping classMapping, MemberMapping<BehaviorEntry> memberMapping, BehaviorEntry newBehavior) { MethodMapping methodMapping = (MethodMapping)memberMapping; classMapping.setMethodObfNameAndSignature( methodMapping.getObfName(), methodMapping.getObfSignature(), newBehavior.getName(), newBehavior.getSignature()); } @Override public boolean hasObfMember(ClassMapping classMapping, BehaviorEntry obfBehavior) { return classMapping.getMethodByObf(obfBehavior.getName(), obfBehavior.getSignature()) != null; } @Override public void removeMemberByObf(ClassMapping classMapping, BehaviorEntry obfBehavior) { classMapping.removeMethodMapping(classMapping.getMethodByObf( obfBehavior.getName(), obfBehavior.getSignature())); } }; } public static <T extends Entry> MemberMatches<T> computeMemberMatches( Deobfuscator destDeobfuscator, Mappings destMappings, ClassMatches classMatches, Doer<T> doer) { MemberMatches<T> memberMatches = new MemberMatches<T>(); // unmatched source fields are easy MappingsChecker checker = new MappingsChecker(destDeobfuscator.getJarIndex()); checker.dropBrokenMappings(destMappings); for(T destObfEntry : doer.getDroppedEntries(checker)) { T srcObfEntry = translate(destObfEntry, classMatches.getUniqueMatches() .inverse()); memberMatches.addUnmatchedSourceEntry(srcObfEntry); } // get matched fields (anything that's left after the checks/drops is // matched( for(ClassMapping classMapping : destMappings.classes()) collectMatchedFields(memberMatches, classMapping, classMatches, doer); // get unmatched dest fields for(T destEntry : doer.getObfEntries(destDeobfuscator.getJarIndex())) if(!memberMatches.isMatchedDestEntry(destEntry)) memberMatches.addUnmatchedDestEntry(destEntry); System.out.println("Automatching " + memberMatches.getUnmatchedSourceEntries().size() + " unmatched source entries..."); // go through the unmatched source fields and try to pick out the easy // matches for(ClassEntry obfSourceClass : Lists.newArrayList(memberMatches .getSourceClassesWithUnmatchedEntries())) for(T obfSourceEntry : Lists.newArrayList(memberMatches .getUnmatchedSourceEntries(obfSourceClass))) { // get the possible dest matches ClassEntry obfDestClass = classMatches.getUniqueMatches().get(obfSourceClass); // filter by type/signature Set<T> obfDestEntries = doer.filterEntries( memberMatches.getUnmatchedDestEntries(obfDestClass), obfSourceEntry, classMatches); if(obfDestEntries.size() == 1) // make the easy match memberMatches.makeMatch(obfSourceEntry, obfDestEntries .iterator().next()); else if(obfDestEntries.isEmpty()) // no match is possible =( memberMatches.makeSourceUnmatchable(obfSourceEntry); } System.out.println(String.format( "Ended up with %d ambiguous and %d unmatchable source entries", memberMatches.getUnmatchedSourceEntries().size(), memberMatches .getUnmatchableSourceEntries().size())); return memberMatches; } private static <T extends Entry> void collectMatchedFields( MemberMatches<T> memberMatches, ClassMapping destClassMapping, ClassMatches classMatches, Doer<T> doer) { // get the fields for this class for(MemberMapping<T> destEntryMapping : doer .getMappings(destClassMapping)) { T destObfField = destEntryMapping.getObfEntry(destClassMapping.getObfEntry()); T srcObfField = translate(destObfField, classMatches.getUniqueMatches() .inverse()); memberMatches.addMatch(srcObfField, destObfField); } // recurse for(ClassMapping destInnerClassMapping : destClassMapping .innerClasses()) collectMatchedFields(memberMatches, destInnerClassMapping, classMatches, doer); } @SuppressWarnings("unchecked") private static <T extends Entry> T translate(T in, BiMap<ClassEntry, ClassEntry> map) { if(in instanceof FieldEntry) return (T)new FieldEntry(map.get(in.getClassEntry()), in.getName(), translate(((FieldEntry)in).getType(), map)); else if(in instanceof MethodEntry) return (T)new MethodEntry(map.get(in.getClassEntry()), in.getName(), translate(((MethodEntry)in).getSignature(), map)); else if(in instanceof ConstructorEntry) return (T)new ConstructorEntry(map.get(in.getClassEntry()), translate(((ConstructorEntry)in).getSignature(), map)); throw new Error("Unhandled entry type: " + in.getClass()); } private static Type translate(Type type, final BiMap<ClassEntry, ClassEntry> map) { return new Type(type, new ClassNameReplacer() { @Override public String replace(String inClassName) { ClassEntry outClassEntry = map.get(new ClassEntry(inClassName)); if(outClassEntry == null) return null; return outClassEntry.getName(); } }); } private static Signature translate(Signature signature, final BiMap<ClassEntry, ClassEntry> map) { if(signature == null) return null; return new Signature(signature, new ClassNameReplacer() { @Override public String replace(String inClassName) { ClassEntry outClassEntry = map.get(new ClassEntry(inClassName)); if(outClassEntry == null) return null; return outClassEntry.getName(); } }); } public static <T extends Entry> void applyMemberMatches(Mappings mappings, ClassMatches classMatches, MemberMatches<T> memberMatches, Doer<T> doer) { for(ClassMapping classMapping : mappings.classes()) applyMemberMatches(classMapping, classMatches, memberMatches, doer); } private static <T extends Entry> void applyMemberMatches( ClassMapping classMapping, ClassMatches classMatches, MemberMatches<T> memberMatches, Doer<T> doer) { // get the classes ClassEntry obfDestClass = classMapping.getObfEntry(); // make a map of all the renames we need to make Map<T, T> renames = Maps.newHashMap(); for(MemberMapping<T> memberMapping : Lists.newArrayList(doer .getMappings(classMapping))) { T obfOldDestEntry = memberMapping.getObfEntry(obfDestClass); T obfSourceEntry = getSourceEntryFromDestMapping(memberMapping, obfDestClass, classMatches); // but drop the unmatchable things if(memberMatches.isUnmatchableSourceEntry(obfSourceEntry)) { doer.removeMemberByObf(classMapping, obfOldDestEntry); continue; } T obfNewDestEntry = memberMatches.matches().get(obfSourceEntry); if(obfNewDestEntry != null && !obfOldDestEntry.getName().equals(obfNewDestEntry.getName())) renames.put(obfOldDestEntry, obfNewDestEntry); } if(!renames.isEmpty()) { // apply to this class (should never need more than n passes) int numRenamesAppliedThisRound; do { numRenamesAppliedThisRound = 0; for(MemberMapping<T> memberMapping : Lists.newArrayList(doer .getMappings(classMapping))) { T obfOldDestEntry = memberMapping.getObfEntry(obfDestClass); T obfNewDestEntry = renames.get(obfOldDestEntry); if(obfNewDestEntry != null) // make sure this rename won't cause a collision // otherwise, save it for the next round and try again // next time if(!doer.hasObfMember(classMapping, obfNewDestEntry)) { doer.setUpdateObfMember(classMapping, memberMapping, obfNewDestEntry); renames.remove(obfOldDestEntry); numRenamesAppliedThisRound++; } } }while(numRenamesAppliedThisRound > 0); if(!renames.isEmpty()) { System.err .println(String .format( "WARNING: Couldn't apply all the renames for class %s. %d renames left.", classMapping.getObfFullName(), renames.size())); for(Map.Entry<T, T> entry : renames.entrySet()) System.err.println(String.format("\t%s -> %s", entry .getKey().getName(), entry.getValue().getName())); } } // recurse for(ClassMapping innerClassMapping : classMapping.innerClasses()) applyMemberMatches(innerClassMapping, classMatches, memberMatches, doer); } private static <T extends Entry> T getSourceEntryFromDestMapping( MemberMapping<T> destMemberMapping, ClassEntry obfDestClass, ClassMatches classMatches) { return translate(destMemberMapping.getObfEntry(obfDestClass), classMatches.getUniqueMatches().inverse()); } }